﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Media.Media3D;
using SosaAnalysisModule.Data;

namespace SosaAnalysisModule
{
    class ReplayModule
    {
        #region CONSTS

        //Board size properties
        private const double BOARDWIDTH = 22;
        private const double BOARDLENGTH = 22;
        private const double BOARDHEIGHT = 1;

        //Board tilt min and max
        private const double MAXPHIAMOUNT = 45;
        private const double MINPHIAMOUNT = 0;

        //Rotation Speed
        private const double ROTATIONSPEED = 5;

        //Fly speed
        private const double FLYMOVESPEED = 0.1;
        private const double FLYSPEED = 0.05;


        //Zoom scale increment
        private const double ZOOMAMOUNT = .05;

        #endregion

        #region Variables

        //States of the camera
        private enum camState
        {
            fly,
            turn
        }
        private camState curCamState = camState.turn;

        //Camera rotation
        private double flyLeftRightRotation;
        private double flyUpDownRotation;

        //Count for current step
        private int CurrentStep = 0;

        //previous mouse cords
        private double PrevMouseX;
        private double PrevMouseY;

        //Total distance of the mouse move
        private double TotalDistX;
        private double TotalDistY;

        //Board size propeties
        private double BoardWidth;
        private double BoardLength;
        private double BoardHeight;

        //Board model properties
        private Color BoardColor;
        private ModelVisual3D Board;
        private Transform3DGroup TransformGroup;
        private QuaternionRotation3D QuadRotation;
        private RotateTransform3D RotationTransform;

        //camera and camera angle variables
        PerspectiveCamera cam;
        Vector3D cameraOriginalLookDirection;
        Point3D cameraOriginalPosition;
        private double Phi;
        private double Theta;
        private double ZoomScale;

        //Camera axies
        private Vector3D ThetaAxis;
        private Vector3D PhiAxis;
        private Vector3D MuAxis;

        //List of all the pegs currently on the board
        private List<Peg> PegList = new List<Peg>();

        //Viewport (render) area for the replay
        private Viewport3D myViewport;

        //Sosa data gatehred from file
        private SosaData myData;

        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new ReplayModule. This inits the everything to "replay" a sosa file
        /// </summary>
        /// <param name="viewport">view port to be used to render the board and pegs to. Also the viewport of the camera</param>
        /// <param name="data">The sosa file data to be used</param>
        public ReplayModule(Viewport3D viewport, SosaData data)
        {
            //Set up the viewport
            myViewport = viewport;

            //Set the sosa data based on the file
            myData = data;

            //Create the board properties
            BoardWidth = BOARDWIDTH;
            BoardLength = BOARDLENGTH;
            BoardHeight = BOARDHEIGHT;
            BoardColor = Colors.White;
            

            //Set up the camera
            MuAxis = new Vector3D(-1, 0, 0);
            ThetaAxis = new Vector3D(0, 1, 0);
            PhiAxis = new Vector3D(0, 0, -1);


            ZoomScale = 1;
            cam = (PerspectiveCamera)myViewport.Camera;
            cam.Position = new Point3D(BoardWidth * 2, cam.Position.Y, 0);
            cameraOriginalPosition = cam.Position;
            cameraOriginalLookDirection = cam.LookDirection;

            //Create the board
            createBoard();

            //Apply any needed transformations
            applyTransformations();

        }

        #endregion

        /// <summary>
        /// Creates a triangle model based on the points passed in.
        /// </summary>
        /// <param name="p0">First point used</param>
        /// <param name="p1">Second point used</param>
        /// <param name="p2">Third point used</param>
        /// <param name="color">Color to make the model</param>
        /// <returns>The triangle model</returns>
        private Model3DGroup createTriangleModel(Point3D p0, Point3D p1, Point3D p2, Color color)
        {
            //Create a mesh to use
            MeshGeometry3D mesh = new MeshGeometry3D();

            //Ass the points to the mesh
            mesh.Positions.Add(p0);
            mesh.Positions.Add(p1);
            mesh.Positions.Add(p2);

            //Add the triangles
            mesh.TriangleIndices.Add(0);
            mesh.TriangleIndices.Add(1);
            mesh.TriangleIndices.Add(2);

            //Calculate the normals
            Vector3D normal = calculateNormal(p0, p1, p2);

            //Then add the normals
            mesh.Normals.Add(normal);
            mesh.Normals.Add(normal);
            mesh.Normals.Add(normal);

            //Create a new material based on the color being used
            Material material = new DiffuseMaterial(new SolidColorBrush(color));

            //Create a 3d model and group
            GeometryModel3D model = new GeometryModel3D(mesh, material);
            Model3DGroup group = new Model3DGroup();
            group.Children.Add(model);

            //return the model group
            return group;
        }

        /// <summary>
        /// Calcualte the normals based on the passed in points
        /// </summary>
        /// <param name="p0">First point to use</param>
        /// <param name="p1">Second point to use</param>
        /// <param name="p2">Third point to use</param>
        /// <returns>The normalized vector</returns>
        ///
        private Vector3D calculateNormal(Point3D p0, Point3D p1, Point3D p2)
        {
            //Calculate the normalization of the vectors
            Vector3D v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
            Vector3D v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);

            //return the cross product of the two vectors
            return Vector3D.CrossProduct(v0, v1);
        }

        /// <summary>
        /// Creates the board to be used for the replay
        /// </summary>
        private void createBoard()
        {
            //Model group for the board 
            Model3DGroup boardGroup = new Model3DGroup();

            //Points needed to make the board
            Point3D p0 = new Point3D((-BoardWidth/2), 0, (BoardLength/2));
            Point3D p1 = new Point3D((BoardWidth/2), 0, (BoardLength/2));
            Point3D p2 = new Point3D((BoardWidth/2), 0, (-BoardLength/2));
            Point3D p3 = new Point3D((-BoardWidth/2), 0, (-BoardLength/2));
            Point3D p4 = new Point3D((-BoardWidth / 2), -BoardHeight, (BoardLength / 2));
            Point3D p5 = new Point3D((BoardWidth / 2), -BoardHeight, (BoardLength / 2));
            Point3D p6 = new Point3D((BoardWidth / 2), -BoardHeight, (-BoardLength / 2));
            Point3D p7 = new Point3D((-BoardWidth / 2), -BoardHeight, (-BoardLength / 2));
            

            //Create front side of the board
            boardGroup.Children.Add(createTriangleModel(p3, p2, p6, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p3, p6, p7, BoardColor));

            //Create the right side of the board
            boardGroup.Children.Add(createTriangleModel(p2, p1, p5, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p2, p5, p6, BoardColor));

            //Create the back side of the board
            boardGroup.Children.Add(createTriangleModel(p1, p0, p4, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p1, p4, p5, BoardColor));

            //Create the left side of the board
            boardGroup.Children.Add(createTriangleModel(p0, p3, p7, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p0, p7, p4, BoardColor));

            //Create the top of the board
            boardGroup.Children.Add(createTriangleModel(p7, p6, p5, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p7, p5, p4, BoardColor));

            //Create the bottom of the board
            boardGroup.Children.Add(createTriangleModel(p2, p3, p0, BoardColor));
            boardGroup.Children.Add(createTriangleModel(p2, p0, p1, BoardColor));

            //Create the board model based on the above points
            Board = new ModelVisual3D();
            Board.Content = boardGroup;
            Board.Transform = new Transform3DGroup();
            TransformGroup = Board.Transform as Transform3DGroup;

            //Add the board to the view port
            myViewport.Children.Add(Board);
        }

        #region Camera controls

        /// <summary>
        /// HOw the camera is controlled based on the keyboard
        /// </summary>
        /// <param name="k">The key that is / was pressed</param>
        public void keyboard(Key k)
        {
            #region Change camera mode

            //Switch between camera modes
            if (k == Key.LeftShift)
            {
                if (curCamState == camState.turn)
                {
                    resetCam();
                    curCamState = camState.fly;
                }
                else if (curCamState == camState.fly)
                {
                    resetCam();
                    curCamState = camState.turn;
                }
            }

            #endregion

            #region Key bindings for fly mode

            //If we are in flying mode
            if (curCamState == camState.fly)
            {

                //Calculate the strafe vector
                Vector3D strafe = Vector3D.CrossProduct(cam.UpDirection, cam.LookDirection);

                //Strafe left
                if (k == Key.A)
                    cam.Position += Vector3D.Multiply(FLYMOVESPEED, strafe);

                //Strafe right
                if (k == Key.D)
                    cam.Position -= Vector3D.Multiply(FLYMOVESPEED, strafe);

                //Fly in or fly out based on keypress
                if (k == Key.W)
                    cam.Position = new Point3D(cam.Position.X + (cam.LookDirection.X * FLYMOVESPEED), cam.Position.Y + (cam.LookDirection.Y * FLYMOVESPEED), cam.Position.Z + (cam.LookDirection.Z * FLYMOVESPEED));
                else if (k == Key.S)
                    cam.Position = new Point3D(cam.Position.X - (cam.LookDirection.X * FLYMOVESPEED), cam.Position.Y - (cam.LookDirection.Y * FLYMOVESPEED), cam.Position.Z - (cam.LookDirection.Z * FLYMOVESPEED));

                if (cam.Position.Y < 1)
                    cam.Position = new Point3D(cam.Position.X,1,cam.Position.Z);
                    

            }

            #endregion
        }

        /// <summary>
        /// Rotates the camera based on the mouse's x and y cords.
        /// Also if the user is pressing the left mosue button
        /// </summary>
        /// <param name="mouseX">The current X position of the mouse</param>
        /// <param name="mouseY">The current Y poistion of the mouse</param>
        public void controlCam(double mouseX, double mouseY)
        {

            //Get the current position of the mouse
            double x = mouseX;
            double y = mouseY;
            
            #region When pressing the left mouse button

            //The the left mouse button is being pressed
            if (Mouse.LeftButton == MouseButtonState.Pressed)
            {
                //Get the distance the mouse traveled since last move
                double distX = x - PrevMouseX;
                double distY = y - PrevMouseY;

                if ((curCamState == camState.turn))
                {
                    #region Turn logic

                    //Calculate the total distance
                    TotalDistX += distX;
                    TotalDistY += distY;

                    //Calculate the amounts to rotate or tilt the board and pegs
                    Theta = TotalDistX / ROTATIONSPEED;
                    Phi = TotalDistY / ROTATIONSPEED;

                    //Limit the amount of tilt the board and pegs can have
                    if (Phi >= MAXPHIAMOUNT)
                        Phi = MAXPHIAMOUNT;
                    else if (Phi <= MINPHIAMOUNT)
                        Phi = MINPHIAMOUNT;

                    //Apply the transformations to the board and pegs
                    applyTransformations();

                    #endregion

                }
                else if (curCamState == camState.fly)
                {
                    //Control the camera using the fly mode camera control
                    flyRotation(distX, distY);
                }
            }
            
            #endregion
            //set the previous x and y of the mouse
            PrevMouseX = x;
            PrevMouseY = y;
        }

        /// <summary>
        /// Function used to calculate the angle and look direction of the view port camera
        /// </summary>
        /// <param name="distX">Distance the mouse has moved on the X axis</param>
        /// <param name="distY">Distance the mouse has moved on the Y axis</param>
        private void flyRotation(double distX, double distY)
        {
                //Calculate the rotation for the left/right and up/down of the camera
                flyLeftRightRotation = ROTATIONSPEED * -distX * FLYSPEED;
                flyUpDownRotation = ROTATIONSPEED * -distY * FLYSPEED;

                //create a new quaternion to base the roation on
                Quaternion QuadRotationX = new Quaternion(ThetaAxis, flyLeftRightRotation);
                Quaternion QuadRotationY;
                
                //Create a vector based on the current look direction 
                Vector3D lookPos = new Vector3D(cam.LookDirection.X, 0, cam.LookDirection.Z);
                
                //Calculate the angle based on the look Pos and the MuAxis
                double angle = Vector3D.AngleBetween(MuAxis, lookPos);

                //CHeck which axis to use for the Y rotation based on the angle calculated
                //Then calculate the rotation
                if (angle >= 45 && angle < 135)
                {
                    if (lookPos.Z > 0)
                        QuadRotationY = new Quaternion(new Vector3D(-1, 0, 0), flyUpDownRotation);
                    else
                        QuadRotationY = new Quaternion(new Vector3D(1, 0, 0), flyUpDownRotation);
                }
                else if (angle >= 135)
                {
                    QuadRotationY = new Quaternion(new Vector3D(0, 0, 1), flyUpDownRotation);
                }
                else
                    QuadRotationY = new Quaternion(PhiAxis, flyUpDownRotation);

                //set up a matrix to use
                Matrix3D rotationMaxtrix = new Matrix3D();

                //Use the quaternions to base the rotation of the maxtirx off of
                rotationMaxtrix.Rotate(QuadRotationX);
                rotationMaxtrix.Rotate(QuadRotationY);

                //set the look direcction of the camera
                cam.LookDirection = rotationMaxtrix.Transform(cam.LookDirection);
        }

        /// <summary>
        /// Used to apply any needed transformations to the board and pegs
        /// </summary>
        private void applyTransformations()
        {
            #region Board transformations

            //Get the transform group of the board
            TransformGroup = Board.Transform as Transform3DGroup;

            //Clear the current transformations
            TransformGroup.Children.Clear();

            //Apply any rotation transformations based on the center of the board
            QuadRotation = new QuaternionRotation3D(new Quaternion(ThetaAxis, Theta));
            RotationTransform = new RotateTransform3D(QuadRotation);
            RotationTransform.CenterX = 0;
            RotationTransform.CenterZ = 0;
            TransformGroup.Children.Add(RotationTransform);

            //Apply any tilt transformations based on the center of the board
            QuadRotation = new QuaternionRotation3D(new Quaternion(PhiAxis, Phi));
            RotationTransform = new RotateTransform3D(QuadRotation);
            RotationTransform.CenterX = 0;
            RotationTransform.CenterZ = 0;
            TransformGroup.Children.Add(RotationTransform);

            //Apply any zoom transformations based on the center of the board
            ScaleTransform3D z = new ScaleTransform3D(ZoomScale, ZoomScale, ZoomScale);
            z.CenterX = 0;
            z.CenterZ = 0;
            TransformGroup.Children.Add(z);

            #endregion

            #region Peg transformations

            //Loop through all the pegs in the peg list and applay the needed transformations
            for (int i = 0;  i < PegList.Count; i++)
            {
                #region Peg Model transformations

                //Create and clear the transformation for the current peg
                TransformGroup = PegList[i].getPegModel().Transform as Transform3DGroup;
                TransformGroup.Children.Clear();

                //Apply any tilt transformations based on the peg
                QuadRotation = new QuaternionRotation3D(new Quaternion(ThetaAxis, Theta));
                RotationTransform = new RotateTransform3D(QuadRotation);
                RotationTransform.CenterX = 0;
                RotationTransform.CenterZ = 0;
                TransformGroup.Children.Add(RotationTransform);

                //Apply any rotation transformations based on the peg
                QuadRotation = new QuaternionRotation3D(new Quaternion(PhiAxis, Phi));
                RotationTransform = new RotateTransform3D(QuadRotation);
                RotationTransform.CenterX = 0;
                RotationTransform.CenterZ = 0;
                TransformGroup.Children.Add(RotationTransform);

                z = new ScaleTransform3D(ZoomScale, ZoomScale, ZoomScale);
                z.CenterX = 0;
                z.CenterZ = 0;
                TransformGroup.Children.Add(z);

                #endregion
            }

            #endregion
        }

        /// <summary>
        /// Used to zoom the camera out
        /// </summary>
        public void zoomOut()
        {
            //If the camera is in fly mode
            if (curCamState == camState.turn)
            {
                //zoom out
                ZoomScale -= ZOOMAMOUNT;

                //apply the needed transformation
                applyTransformations();
            }
        }

        /// <summary>
        /// Used to zoom the camera in
        /// </summary>
        public void zoomIn()
        {
            //If the camera is in fly mode
            if (curCamState == camState.turn)
            {
                //Zoom in
                ZoomScale += ZOOMAMOUNT;

                //Apply the needed transformations
                applyTransformations();
            }
        }

        /// <summary>
        /// Reset the camera to its original settings
        /// </summary>
        public void resetCam()
        {
            //Reset all the variables used to guide the camera
            ZoomScale = 1;
            TotalDistX = 0;
            TotalDistY = 0;
            Phi = 0;
            Theta = 0;

            //restore the starting position and look direciton of the camera
            cam.LookDirection = cameraOriginalLookDirection;
            cam.Position = cameraOriginalPosition;

            //Clear any transformations on the board
            Transform3DGroup boardTrans = Board.Transform as Transform3DGroup;
            boardTrans.Children.Clear();

            //Clear all the transformations applied to the pegs in the peg list
            for (int i = 0; i < PegList.Count; i++)
            {
                Transform3DGroup pegTrans = PegList[i].getPegModel().Transform as Transform3DGroup;
                pegTrans.Children.Clear();
            }
        }

        #endregion

        #region Step buttons

        /// <summary>
        /// Used to step backwards through the current replay
        /// </summary>
        /// <param name="l">Debug list box used</param>
        public void prevStep(ListBox info)
        {

            //if the current step is the first one
            if (CurrentStep == 0)
            {
                //CLear the board of all pegs
                firstStep(info);
            }
            else if (CurrentStep > 0 && CurrentStep < myData.SubjectActions.Length)
            {
                //needed bool for peg remove / existance
                bool exist = true;
                int placement = 0;
                string removeLabel = "";

                //Loop through the sosa data and check if a peg does not exist prior to the current point in the replay
                for (int n = CurrentStep; n > 0; n--)
                {
                    //check if we are looking at the right peg
                    if ((PegList[PegList.Count - 1].getLabel() == myData.SubjectActions[n].Label) && (myData.SubjectActions[n].From.X == -14.0) && (myData.SubjectActions[n].From.Y == 14.0))
                    {
                        //If we match the current step of the sosa file remove the peg
                        if (CurrentStep == n)
                        {
                            //Information statement
                            info.Items.Add("Peg " + myData.SubjectActions[n].Label + " was removed.");

                            placement = n;
                            removeLabel = myData.SubjectActions[n].Label;

                            //Say the peg does not exist and break from the for loop
                            exist = false;
                            break;
                        }
                    }
                }

                //If the peg does exist past the current point, move it back to the right position
                if (exist == true)
                {
                    //Decrement the current step count
                    CurrentStep--;

                    //Move the peg to its previous position
                    for (int i = 0; i < PegList.Count; i++)
                    {
                        //Make sure are moving the right peg
                        if (PegList[i].getLabel() == myData.SubjectActions[CurrentStep].Label)
                        {
                            PegList.RemoveAt(i);
                            myViewport.Children.RemoveAt(i + 2);

                            Peg p = new Peg(myViewport);
                            p.createPeg(myData.SubjectActions[CurrentStep].Label, myData.SubjectActions[CurrentStep].From.X, myData.SubjectActions[CurrentStep].From.Y);
                            PegList.Add(p);

                            //information statments
                            info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + " moved");
                            info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + "'s position - X :" + p.getX() + "; Y : " + p.getY() + ".");

                            //break from the for loop
                            break;
                        }
                    }
                }
                else
                {
                    //Make sure to remove the right peg. Also a double check for for an exception error
                    for(int i = 0; i < PegList.Count; i++)
                    {
                        if (PegList[i].getLabel() == removeLabel)
                            placement = i;
                    }

                    //Remove the peg
                    PegList.RemoveAt(placement);
                    myViewport.Children.RemoveAt(placement + 2);
                }
            }
            //If we are on the final step of the action list
            else if(CurrentStep == myData.SubjectActions.Length)
            {
                //Move the last peg of the action list to its previous position
                PegList.RemoveAt(PegList.Count - 1);
                myViewport.Children.RemoveAt(myViewport.Children.Count - 1);

                Peg p = new Peg(myViewport);
                p.createPeg(myData.SubjectActions[CurrentStep - 1].Label, myData.SubjectActions[CurrentStep - 1].From.X, myData.SubjectActions[CurrentStep - 1].From.Y);
                PegList.Add(p);

                //information statements
                info.Items.Add("Peg " + myData.SubjectActions[CurrentStep - 1].Label + " moved");
                info.Items.Add("Peg " + myData.SubjectActions[CurrentStep - 1].Label + "'s position - X :" + p.getX() + "; Y : " + p.getY() + ".");

                //deremnt the step count 
                CurrentStep--;
            }
            
            //Apply any needed transformations
            applyTransformations();

        }

        /// <summary>
        /// Used to step foward through the current replay
        /// </summary>
        /// <param name="l"></param>
        public void nextStep(ListBox info)
        {
            //If thre are move steps to take for the replay do them
            if(CurrentStep < myData.SubjectActions.Length)
            {
                //Needed vars
                bool exist = false;
                int position = -1;

                //Leep through the current peg list count
                for (int i = 0; i < PegList.Count; i++)
                {
                    //Check if a peg already exists in the peg lisr
                    if (PegList[i].getLabel() != myData.SubjectActions[CurrentStep].Label)
                        exist = false;
                    else
                    {
                        //debug statement 
                        //info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + " EXISTS!");
                        
                        //Say it exists and keep track for the position found 
                        exist = true;
                        position = i;

                        //break from the for loop
                        break;
                    }
                }

                //IF the peg does not exist add it to the peg list
                if (exist == false)
                {
                    //create a new peg and add it
                    Peg p = new Peg(myViewport);
                    p.createPeg(myData.SubjectActions[CurrentStep].Label, myData.SubjectActions[CurrentStep].From.X, myData.SubjectActions[CurrentStep].From.Y);
                    PegList.Add(p);

                    //information statements
                    info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + " was added to the board.");
                    info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + "'s position - X :" + p.getX() + "; Y : " + p.getY() + ".");

                }
                else
                {
                    //Remove replace the need peg from the list and board
                    myViewport.Children.RemoveAt(position + 2);
                    PegList.RemoveAt(position);

                    //replace the peg
                    Peg p = new Peg(myViewport);
                    p.createPeg(myData.SubjectActions[CurrentStep].Label, myData.SubjectActions[CurrentStep].To.X, myData.SubjectActions[CurrentStep].To.Y);
                    PegList.Add(p);

                    //information statements
                    info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + " moved");
                    info.Items.Add("Peg " + myData.SubjectActions[CurrentStep].Label + "'s position - X :" + p.getX() + "; Y : " + p.getY() + ".");

                    CurrentStep++;
                }
            }

            //Apply any transformations that are needed
            applyTransformations();
        }

        /// <summary>
        /// Function used to set the board to the first step or initial state
        /// </summary>
        public void firstStep(ListBox info)
        {
            //Clear out the list box
            info.Items.Clear();

            //Set the current step to zero
            CurrentStep = 0;

            //Remove all the pegs from the board
            for (int i = 0; i < PegList.Count; i++)
            {
                myViewport.Children.RemoveAt(2);
            }

            //Clear the pegs from the peg list
            PegList.Clear();
        }

        /// <summary>
        /// Used to get the final step of the sosa data replay
        /// </summary>
        public void lastStep(ListBox info)
        {

            //Clear the board of all pegs
            firstStep(info);

            //Loop through the subject actions and add the pegs
            for (int i = 0; i < myData.SubjectActions.Length + myData.PresentationOrder.Length; i++)
            {
                nextStep(info);
            }

            //Set the current step to final action
            CurrentStep = myData.SubjectActions.Length;

            info.Items.Add("C : " + CurrentStep);

            //apply any needed transformations
            applyTransformations();
        }

        #endregion

    }
}
